{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Digest  摘要认证\n",
    "\n",
    "## 摘要认证介绍 \n",
    "\n",
    "ISAPI 协议基于 HTTP REST 架构，协议交互需要安全认证，Digest 摘要认证比 Basic 基础认证的安全级别更高： \n",
    "\n",
    "1）通过传递用户名、密码等计算出来的摘要来解决明文方式在网络上发送密码的问题。 \n",
    "\n",
    "2）通过服务产生随机数 nonce 的方式可以防止恶意用户捕获并重放认证的握手过程。 \n",
    "\n",
    "---\n",
    "\n",
    "### 1.1 认证握手过程 \n",
    "\n",
    "1. 客户端发出一个没有认证证书的请求\n",
    "\n",
    "```http\n",
    "GET /ISAPI/Security/userCheck HTTP/1.1 \n",
    "Accept: text/html, application/xhtml+xml, */* \n",
    "Accept-Language: zh-CN \n",
    "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko \n",
    "Accept-Encoding: gzip, deflate \n",
    "Host: 10.18.37.12 \n",
    "Connection: Keep-Alive \n",
    "```\n",
    "注：此处示例为用户名密码校验的ISAPI协议命令（GET方法），每次下发新的命令都需要重新认证。 \n",
    "\n",
    "---\n",
    "\n",
    "2. 服务器产生一个随机数nonce，并且将该随机数放在WWW-Authenticate响应头，与服务器支持的认证算法列表，认证的域realm一起发送给客户端。 \n",
    "\n",
    "```http\n",
    "HTTP/1.1 401 Unauthorized \n",
    "Date: Wed, 30 May 2018 19:16:52 GMT \n",
    "Server: App-webs/ \n",
    "Content-Length: 178 \n",
    "Content-Type: text/html \n",
    "Connection: keep-alive \n",
    "Keep-Alive: timeout=10, max=99 \n",
    "WWW-Authenticate: Digest qop=\"auth\", realm=\"IP Camera(12345)\", nonce=\"4e5749344e7a4d794e544936596a4933596a51784e44553d\", stale=\"FALSE\" \n",
    "```\n",
    "注：401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式，**此处设备只支持Digest摘要认证方式。**\n",
    "\n",
    "```http\n",
    "HTTP/1.1 401 Unauthorized \n",
    "Date: Wed, 30 May 2018 19:23:32 GMT \n",
    "Server: App-webs/ \n",
    "Content-Length: 178 \n",
    "Content-Type: text/html \n",
    "Connection: keep-alive \n",
    "Keep-Alive: timeout=10, max=99 \n",
    "WWW-Authenticate: Digest qop=\"auth\", realm=\"IP Camera(12345)\", nonce=\"4f5455784e4452684f544136596a49344d54566a4f57553d\", stale=\"FALSE\" \n",
    "WWW-Authenticate: Basic realm=\"IP Camera(12345)\" \n",
    "```\n",
    "注：401 Unauthorized表示认证失败、未授权。返回的WWW-Authenticate表示设备支持的认证方式，**此处设备同时支持Digest摘要认证和Basic认证两种方式。stale表示nonce值是(TRUE)否(FALSE)过期，如果过期会生成新的随机数。**\n",
    "\n",
    "---\n",
    "\n",
    "3. 客户端接收到401响应表示需要进行认证，选择一个算法（**目前只支持MD5**）生成一个消息摘要（message digest，该摘要包含用户名、密码、给定的nonce值、HTTP方法以及所请求的URL），将摘要放到Authorization的请求头中重新发送命令给服务器。**如果客户端要对服务器也进行认证，可以同时发送客户端随机数cnonce，客户端是否需要认证，通过报文里面的qop值进行判断**，详见1.2章节介绍。 \n",
    "\n",
    "```http\n",
    "GET /ISAPI/Security/userCheck HTTP/1.1 \n",
    "Accept: text/html, application/xhtml+xml, */* \n",
    "Accept-Language: zh-CN \n",
    "User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; Trident/7.0; rv:11.0) like Gecko \n",
    "Accept-Encoding: gzip, deflate \n",
    "Host: 10.18.37.12 \n",
    "Connection: Keep-Alive \n",
    "Authorization: Digest username=\"admin\",realm=\"IP \n",
    "Camera(12345)\",nonce=\"595463314d5755354d7a4936596a49344f475a6a5a44453d\",uri=\"/ISAPI/Security/userCheck\",cnonce=\"011e08f6c9d5b3e13acfa810ede73ecc\",nc=00000001,response=\"82091ef5aaf9b54118b4887f8720ae06\",qop=\"auth\" \n",
    "```\n",
    "---\n",
    "\n",
    "4. 服务接收到摘要，选择算法以及掌握的数据，**重新计算新的摘要跟客户端传输的摘要进行比较，验证是否匹配**，若客户端反过来用客户端随机数对服务器进行质询，就会创建客户端摘要，服务可以预先将下一个随机数计算出来，提前传递给客户端，通过 `Authentication-Info` 发送下一个随机数。该步骤选择实现。 \n",
    "\n",
    "```http\n",
    "HTTP/1.1 200 OK \n",
    "Date: Wed, 30 May 2018 19:32:49 GMT \n",
    "Server: App-webs/ \n",
    "Content-Length: 132 \n",
    "Connection: keep-alive \n",
    "Keep-Alive: timeout=10, max=98 \n",
    "Content-Type: text/xml \n",
    "\n",
    "<?xml version=\"1.0\" encoding=\"UTF-8\"?> \n",
    "<userCheck> \n",
    "<statusValue>200</statusValue> \n",
    "<statusString>OK</statusString> \n",
    "</userCheck> \n",
    "```\n",
    "\n",
    "注：响应200 OK表示认证成功。 \n",
    "\n",
    "详细说明请参考**RFC 2617**规范文档。 \n",
    "\n",
    "---\n",
    "\n",
    "## 摘要计算过程 \n",
    "\n",
    "在说明如何计算摘要之前，先说明参加摘要计算的信息块。信息块主要有两种： \n",
    "\n",
    "1. 表示与安全相关的数据的A1 \n",
    "\n",
    "  A1中的数据时密码和受保护信息的产物，它包括用户名、密码、保护域和随机数等内容，A1只涉及安全信息，与底层报文自身无关。 \n",
    "\n",
    "  **若算法是：MD5, 则**\n",
    "```\n",
    "A1=<user>:<realm>:<password>\n",
    "```\n",
    "\n",
    "  **若算法是：MD5-sess, 则**\n",
    "\n",
    "```\n",
    "A1=MD5(<user>:<realm>:<password>):<nonce>:<cnonce> \n",
    "```\n",
    "\n",
    "2. 表示与报文相关的数据的A2 \n",
    "\n",
    "  A2表示是与报文自身相关的信息，比如URL，请求反复和报文实体的主体部分，A2加入摘要计算主要目的是有助于防止反复，资源或者报文被篡改。 \n",
    "\n",
    "  **若 qop 未定义或者 auth：**\n",
    "```\n",
    "A2=<request-method>:<uri-directive-value> \n",
    "```\n",
    "  **若 qop 为 auth:-int**\n",
    "```\n",
    "A2=<request-method>:<uri-directive-value>:MD5(<request-entity-body>) \n",
    "```\n",
    "  注：`<uri-directive-value>`为完整的协议命令 URI，比如“/ISAPI/Security/userCheck”。 \n",
    "\n",
    "下面定义摘要的计算规则： \n",
    "    \n",
    "- 若 qop 没有定义： \n",
    "```\n",
    "response=MD5(MD5(A1):<nonce>:MD5(A2)) \n",
    "```\n",
    "\n",
    "- 若 qop 为 auth:\n",
    "```\n",
    "response=MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2)) \n",
    "```\n",
    "- 若 qop 为 auth-int: \n",
    "```\n",
    "response= MD5(MD5(A1):<nonce>:<nc>:<cnonce>:<qop>:MD5(A2)) \n",
    "```\n",
    "\n",
    "## 随机数的生成 \n",
    "\n",
    "`RFC2617`建议采用这个假想的随机数公式： \n",
    "\n",
    "```\n",
    "nonce = BASE64(time-stamp MD5(time-stamp \":\" ETag \":\" private-key)) \n",
    "```\n",
    "\n",
    "其中: \n",
    "  \n",
    "  `time-stamp`是服务器产生的时间戳或者其他不会重复的序列号，`ETag`是与所请求实体有关的HTTP `ETag`首部的值，priviate-key是只有服务器知道的数据。 \n",
    "  \n",
    "  这样，服务器就可以收到客户端的认证首部之后重新计算散列部分，**如果结果与那个首部的随机数不符，或者是时间戳的值不够新，**就可以拒绝请求，服务器可以通过这种方式来限制随机数的有效持续时间。 \n",
    " \n",
    "  包括了ETag可以防止对已经更新资源版本的重放请求。注意：在随机数中包含客户端IP，服务器好像就可以限制原来获取此随机数的客户端重用这个随机数了，但这会破坏代理集群的工作，使用代理集群时候，来自单个用户的多条请求通常会经过不同的代理进行传输，而且IP地址欺骗实现起来也不复杂。 \n",
    "\n",
    "对于我司设备，认证的随机数超时时间如下所示： \n",
    "\n",
    "**认证返回的 nonce 是 3s 超时；**\n",
    "\n",
    "非认证返回的 none 是 30s 超时。 \n",
    "\n",
    "\n",
    "# 摘要认证开发实现方法\n",
    "\n",
    "目前以 HTTP 协议为例。\n",
    "\n",
    "## C++ 实现\n",
    "\n",
    "MD5 算法：\n",
    "\n",
    "```c++\n",
    "void __stdcall mprGetMD5Hash(unsigned char *buf, int length, unsigned char *digest)\n",
    "{\n",
    "HMAC_MD5_CTX context;\n",
    "unsigned char hash[CRYPT_HASH_SIZE];\n",
    "const char *hex = \"0123456789abcdef\";\n",
    "char *r;\n",
    "char result[(CRYPT_HASH_SIZE * 2) + 1];\n",
    "int i;\n",
    "/*\n",
    "* Take the MD5 hash of the string argument.\n",
    "*/\n",
    "hmac_MD5Init(&context);\n",
    "hmac_MD5Update(&context, (unsigned char*) buf, (unsigned int) length);\n",
    "hmac_MD5Final(hash, &context);\n",
    "for (i = 0, r = result; i < 16; i++) {\n",
    "*r++ = hex[hash[i] >> 4];\n",
    "*r++ = hex[hash[i] & 0xF];\n",
    "}\n",
    "*r = '\\0';\n",
    "strcpy(digest, result);\n",
    "}\n",
    "```\n",
    "\n",
    "摘要认证实现：\n",
    "```c++\n",
    "char szSrc[HTTP_DEGIST_SRC_LEN];\n",
    "memset(szSrc, 0, HTTP_DEGIST_SRC_LEN);\n",
    "\n",
    "sprintf(szSrc, \"%s:%s:%s\", m_strUserName, m_szHttpRealm, m_strUserPwsd);\n",
    "char szHA1[HTTP_HA_LEN] = {0}; // 与安全相关的数据的A1\n",
    "mprGetMD5Hash((unsigned char *)szSrc, min(HTTP_DEGIST_SRC_LEN, strlen(szSrc)),(unsigned char *)szHA1);\n",
    "\n",
    "memset(szSrc, 0, HTTP_DEGIST_SRC_LEN);\n",
    "if (m_iProtocolCommand == ISAPI_GET)\n",
    "{\n",
    "    sprintf(szSrc, \"GET:%s\", m_szProtocolUrl);\n",
    "}\n",
    "else if (m_iProtocolCommand == ISAPI_PUT)\n",
    "{\n",
    "    sprintf(szSrc, \"PUT:%s\", m_szProtocolUrl);\n",
    "}\n",
    "else if (m_iProtocolCommand == ISAPI_POST)\n",
    "{\n",
    "    sprintf(szSrc, \"POST:%s\", m_szProtocolUrl);\n",
    "}\n",
    "else\n",
    "{\n",
    "    return;\n",
    "}\n",
    "\n",
    "char szHA2[HTTP_HA_LEN] = {0}; // 与报文相关的数据的A2\n",
    "mprGetMD5Hash((unsigned char *)szSrc, min(HTTP_DEGIST_SRC_LEN, strlen(szSrc)),(unsigned char *)szHA2);\n",
    "memset(szSrc, 0, HTTP_DEGIST_SRC_LEN);\n",
    "sprintf(szSrc, \"%s:%s:%s\", szHA1, m_szHttpNonce, szHA2);\n",
    "\n",
    "char szResponse[HTTP_HA_LEN] = {0}; //Authorization 请求头里面的response 内容\n",
    "mprGetMD5Hash((unsigned char *)szSrc, min(HTTP_DEGIST_SRC_LEN, strlen(szSrc)),(unsigned char *)szResponse);\n",
    "\n",
    "// 下发命令并且带Authorization 摘要认证内容\n",
    "const char *pFormat = \"%s HTTP/1.1\\r\\nHost:%s\\r\\nAccept: text/html, application/xhtml+xml,\n",
    "*/*\\r\\nUser-Agent: %s\\r\\nAuthorization: Digest username=\\\"%s\\\", realm=\\\"%s\\\", nonce=\\\"%s\\\", uri=\\\"%s\\\",\n",
    "response=\\\"%s\\\"\\r\\n\\r\\n\";\n",
    "sprintf(szSendBuf, pFormat, m_strUrl.GetBuffer(0), m_szDeviceIP, g_pUserAgent, m_strUserName, m_szHttpRealm,\n",
    "m_szHttpNonce, m_szProtocolUrl, szResponse);\n",
    "\n",
    "```\n",
    "\n",
    "## C# 实现\n",
    "\n",
    "C#语言开发有现成的 WebClient 类可以直接使用，所属命名空间为 System.Net。\n",
    "\n",
    "\n",
    "```C#\n",
    "using System.Net;\n",
    "public static WebClient m_webClient = new WebClient();\n",
    "\n",
    "m_webClient.Credentials = new NetworkCredential(sUserName, sPassword); //设备登录用户名密码\n",
    "m_webClient.BaseAddress = \"http://\" + sDeviceAddress + \":\" + iPort; //设备 IP 地址和端口\n",
    "strUrl = \"/ISAPI/Security/userCheck\";\n",
    "string loginResult = \"\";\n",
    "loginResult = m_webClient.DownloadString(strUrl); //WebClient 类中方法,以 String 形式下载指定资源\n",
    "```\n",
    "\n",
    "DownloadString 返回的结果是字符串，数据可能会异常，使用 UploadData 返回数据可以在 Byte 数组缓\n",
    "冲区里面，如下所示：\n",
    "\n",
    "```C#\n",
    "public bool Upload(ICredentials Credentials, String requestUrl, out String responseText)\n",
    "{\n",
    "    WebClient webClient = new WebClient();\n",
    "    webClient.Credentials = Credentials;\n",
    "    webClient.Headers.Add(\"Content-Type\", \"multipart/form-data; boundary=\" + boundary);\n",
    "    byte[] responseBytes;\n",
    "    byte[] bytes = MergeContent();\n",
    "    try\n",
    "    {\n",
    "        responseBytes = webClient.UploadData(requestUrl, bytes);\n",
    "        responseText = System.Text.Encoding.UTF8.GetString(responseBytes);\n",
    "    \treturn true;\n",
    "    }\n",
    "    catch (WebException ex)\n",
    "    {\n",
    "        Stream responseStream = ex.Response.GetResponseStream();\n",
    "        responseBytes = new byte[ex.Response.ContentLength];\n",
    "        responseStream.Read(responseBytes, 0, responseBytes.Length);\n",
    "    }\n",
    "    responseText = System.Text.Encoding.UTF8.GetString(responseBytes);\n",
    "    return false;\n",
    "}\n",
    "\n",
    "```\n",
    "\n",
    "\n",
    "```C#\n",
    "WebClient client = new WebClient();\n",
    "client.Credentials = new NetworkCredential(strUserName, strPassword, struDeviceInfo.chDeviceIP);\n",
    "client.Headers.Add(\"Content-Type\", \"application/x-www-form-urlencoded; charset=UTF-8\");\n",
    "client.Headers.Add(\"ContentLength\", bytes.Length.ToString());\n",
    "byte[] responseData = client.UploadData(strUrl, \"POST\", bytes);\n",
    "string strFD = System.Text.Encoding.UTF8.GetString(responseData);\n",
    "//获取json参数\n",
    "string[] szjsontemp = Regex.Split(strFD, \"\\r\\n\", RegexOptions.IgnoreCase);\n",
    "//解析json数据\n",
    "m_jsonTaskID = JsonConvert.DeserializeObject<jsonTaskID>(strFD);\n",
    "```\n",
    "\n",
    "\n",
    "## Java实现\n",
    "\n",
    "Java 语言开发有现成的 httpclient 工具类可以直接使用。\n",
    "\n",
    "\n",
    "```java\n",
    "import org.apache.commons.httpclient.UsernamePasswordCredentials;\n",
    "import org.apache.commons.httpclient.auth.AuthScope;\n",
    "\n",
    "strDevicePort = \"80\";\n",
    "UsernamePasswordCredentials creds = new UsernamePasswordCredentials(UsertextField.getText(),\n",
    "PasstextField.getText()); //设备登录用户名密码，摘要认证方式\n",
    "HTTPClientUtil.client.getState().setCredentials(AuthScope.ANY, creds);\n",
    "//登录校验代码待补充\n",
    "String strUrl = \"/ISAPI/Security/userCheck\";\n",
    "strOut= HTTPClientUtil.doGet(\"http://\"+IPtextField.getText()+\":\"+strUrl, null);\n",
    "```\n",
    "\n",
    "```java\n",
    "import org.apache.commons.httpclient.HttpClient;\n",
    "import org.apache.commons.httpclient.methods.GetMethod;\n",
    "import org.apache.commons.httpclient.methods.PutMethod;\n",
    "import org.apache.commons.httpclient.methods.PostMethod;\n",
    "import org.apache.commons.httpclient.methods.DeleteMethod;\n",
    "public static HttpClient client = new HttpClient();\n",
    "\n",
    "public class HTTPClientUtil{\n",
    "    public static String doGet(String url, String charset) throws Exception\n",
    "    {\n",
    "        GetMethod method = new GetMethod(url);\n",
    "        method.setDoAuthentication(true);\n",
    "        int statusCode = client.executeMethod(method);\n",
    "        byte[] responseBody =\n",
    "        method.getResponseBodyAsString().getBytes(method.getResponseCharSet());\n",
    "        //在返回响应消息使用编码(utf-8 或 gb2312)\n",
    "        String response = new String(responseBody, \"utf-8\");\n",
    "        //释放连接\n",
    "        method.releaseConnection();\n",
    "        return response;\n",
    "    }\n",
    "}\n",
    "```"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Java",
   "language": "java",
   "name": "java"
  },
  "language_info": {
   "codemirror_mode": "java",
   "file_extension": ".jshell",
   "mimetype": "text/x-java-source",
   "name": "Java",
   "pygments_lexer": "java",
   "version": "12.0.2+10"
  },
  "toc": {
   "base_numbering": 1,
   "nav_menu": {},
   "number_sections": true,
   "sideBar": true,
   "skip_h1_title": false,
   "title_cell": "Table of Contents",
   "title_sidebar": "Contents",
   "toc_cell": false,
   "toc_position": {
    "height": "calc(100% - 180px)",
    "left": "10px",
    "top": "150px",
    "width": "191.045px"
   },
   "toc_section_display": true,
   "toc_window_display": true
  },
  "varInspector": {
   "cols": {
    "lenName": 16,
    "lenType": 16,
    "lenVar": 40
   },
   "kernels_config": {
    "python": {
     "delete_cmd_postfix": "",
     "delete_cmd_prefix": "del ",
     "library": "var_list.py",
     "varRefreshCmd": "print(var_dic_list())"
    },
    "r": {
     "delete_cmd_postfix": ") ",
     "delete_cmd_prefix": "rm(",
     "library": "var_list.r",
     "varRefreshCmd": "cat(var_dic_list()) "
    }
   },
   "types_to_exclude": [
    "module",
    "function",
    "builtin_function_or_method",
    "instance",
    "_Feature"
   ],
   "window_display": false
  }
 },
 "nbformat": 4,
 "nbformat_minor": 2
}
